52  数据框拼接

52.1 引言多源数据整合的挑战

在真实的数据分析项目中,数据往往分散在多个来源中。对于金融分析师而言,将不同来源的数据整合在一起是日常工作的核心挑战。

52.1.1 金融数据整合的典型场景

多源数据整合的必要性:

  • 行情数据:来自交易所的实时价格、成交量数据
  • 财务数据:来自公司财报的资产负债表、利润表数据
  • 宏观数据:来自统计部门的GDP、CPI、利率数据
  • 情绪数据:来自新闻舆情、社交媒体的投资者情绪指标

52.1.2 数据整合的核心问题

为什么数据整合如此复杂?

  1. 粒度不匹配:日频行情数据 vs 季频财务数据
  2. 时间对齐:不同市场的交易日历不同(如A股 vs 美股)
  3. 键值识别:如何正确匹配同一公司的不同数据源?
  4. 重复数据:同一指标可能来自多个提供商,如何去重?
  5. 性能瓶颈:大规模数据集的合并操作可能极其耗时

本章学习目标: - 理解concat、merge、join的本质区别与应用场景 - 掌握不同类型连接(inner/left/right/outer)的数学含义 - 学习金融时间序列数据的对齐与聚合技术 - 了解大数据环境下的性能优化策略 - 掌握多级索引在复杂数据整合中的应用

52.2 数据拼接的数学基础

52.2.1 垂直拼接(行向堆叠)

定义:将多个数据集沿行方向(纵向)堆叠,增加观测数量。

设两个数据矩阵: \[ D_1 = \begin{bmatrix} x_{11} & x_{12} \\ x_{21} & x_{22} \\ \vdots & \vdots \\ x_{n1} & x_{n2} \end{bmatrix}, \quad D_2 = \begin{bmatrix} y_{11} & y_{12} \\ y_{21} & y_{22} \\ \vdots & \vdots \\ y_{m1} & y_{m2} \end{bmatrix} \]

垂直拼接结果: \[ D_{\text{concat}} = \begin{bmatrix} D_1 \\ D_2 \end{bmatrix} = \begin{bmatrix} x_{11} & x_{12} \\ \vdots & \vdots \\ x_{n1} & x_{n2} \\ y_{11} & y_{12} \\ \vdots & \vdots \\ y_{m1} & y_{m2} \end{bmatrix} \]

前提条件:两个数据集必须具有相同的列结构(相同列名和数据类型)。

52.2.2 水平拼接(列向合并)

定义:将多个数据集沿列方向(横向)合并,增加变量数量。

\[ D_{\text{merge}} = [D_1 \mid D_2] = \begin{bmatrix} x_{11} & x_{12} & y_{11} & y_{12} \\ \vdots & \vdots & \vdots & \vdots \\ x_{n1} & x_{n2} & y_{n1} & y_{n2} \end{bmatrix} \]

前提条件:两个数据集必须具有相同的行数或可通过键值对齐。

52.2.3 关系代数基础

Pandas的merge操作基于关系代数(Relational Algebra)中的连接(Join)运算:

\[ R \bowtie_{\theta} S = \{ (r, s) \in R \times S \mid \theta(r, s) \} \]

其中: - \(R, S\): 两个关系(数据表) - \(\bowtie\): 连接运算符 - \(\theta\): 连接条件(通常是键值相等)

52.3 concat函数垂直拼接的首选工具

52.3.1 基础语法与参数

平台任务1解答代码

以下代码与教学平台任务要求完全一致:

列表 52.1
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)#从外部导入Sheet1的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_JantoMar.head()) #查看前五行数据

print(price_JantoMar.tail()) #查看后五行数据

price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)##从外部导入Sheet2的5只股票信息链接为https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_AprtoJui.head()) #查看前五行数据

print(price_AprtoJui.tail()) #查看后五行数据

平台任务2解答代码

以下代码与教学平台任务要求完全一致:

列表 52.2
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

# 从Excel文件读取数据存入price_JantoMar
price_JantoMar = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)

# 从Excel文件读取数据存入price_AprtoJui
price_AprtoJui = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)

price_JantoJul = pd.concat([price_JantoMar,price_AprtoJui],axis=0) #使用concat函数按行拼接

print(price_JantoJul.head())  #前五行数据

print(price_JantoJul.tail())  #后五行数据

平台任务3解答代码

以下代码与教学平台任务要求完全一致:

列表 52.3
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_3stocks.head()) #查看前五行数据

print(price_3stocks.tail()) #查看后五行数据

price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0) #导入数据Sheet1 链接https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx

print(price_2stocks.head()) #查看前五行数据

print(price_2stocks.tail()) #查看后五行数据 

平台任务4解答代码

以下代码与教学平台任务要求完全一致:

列表 52.4
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库

# 从Excel文件读取数据存入price_3stocks
price_3stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet1",header=0,index_col=0)

# 从Excel文件读取数据存入price_2stocks
price_2stocks = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/20220821/xlsx/1561220500528062464.xlsx",sheet_name="Sheet2",header=0,index_col=0)

price_5stocks_concat = pd.concat([price_3stocks,price_2stocks],axis=1) #使用concat函数按列拼接

print(price_5stocks_concat.head()) #查看前五行数据

price_5stocks_merge = pd.merge(left=price_3stocks,right=price_2stocks,left_index=True,right_index=True) #使用merge函数按列拼接

print(price_5stocks_merge.head()) #查看前五行数据

price_5stocks_join = price_3stocks.join(price_2stocks,on="日期") #用join函数按列拼接

print(price_5stocks_join.head()) #查看前五行数据
列表 52.5
# =============================================================================
# 题目:使用concat进行垂直拼接
# =============================================================================
# 本任务演示如何使用pd.concat()函数将多个数据框垂直拼接(沿行方向堆叠)
# 场景:将多只股票的收益率数据合并成一个长格式数据框

# ==================== 导入必要的库 ====================
import pandas as pd  # Pandas数据分析库
import numpy as np  # NumPy数值计算库

# ==================== 创建股票A的收益率数据 ====================
# 场景:贵州茅台(600519.SH)连续3个交易日的收益率数据
# pd.date_range():生成日期范围,'2024-01-01'为起始日期,periods=3表示生成3个日期
stock_a_returns = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=3),  # 生成3个连续日期
    '股票代码': ['600519.SH'] * 3,  # 股票代码重复3次(列表乘法)
    '收益率': [0.02, -0.01, 0.03]  # 3个交易日的收益率:2%, -1%, 3%
})

# ==================== 创建股票B的收益率数据 ====================
# 场景:五粮液(000858.SZ)接下来的3个交易日收益率数据
# 注意:这里的起始日期是'2024-01-04',正好接续股票A的最后日期
stock_b_returns = pd.DataFrame({
    '日期': pd.date_range('2024-01-04', periods=3),  # 从2024-01-04开始生成3个日期
    '股票代码': ['000858.SZ'] * 3,  # 五粮液的股票代码
    '收益率': [0.01, 0.02, -0.02]  # 3个交易日的收益率:1%, 2%, -2%
})

# ==================== 使用concat进行垂直拼接 ====================
# pd.concat():拼接函数,将多个数据框沿指定轴拼接
# 参数说明:
#   [stock_a_returns, stock_b_returns]:要拼接的数据框列表
#   ignore_index=True:忽略原始索引,重新生成从0开始的连续索引
#     - False(默认):保留原始索引,可能出现重复索引
#     - True:重置索引为0, 1, 2, ..., n-1
all_returns = pd.concat([stock_a_returns, stock_b_returns], ignore_index=True)

# ==================== 打印原始数据 ====================
print('股票A数据:')
print(stock_a_returns)
# 输出解读:贵州茅台3天的收益率数据,索引为0, 1, 2

print('\n股票B数据:')
print(stock_b_returns)
# 输出解读:五粮液3天的收益率数据,索引为0, 1, 2

# ==================== 打印拼接结果 ====================
print('\n拼接结果:')
print(all_returns)
# 输出解读:两个数据框垂直拼接,共6行数据
# ignore_index=True确保索引从0到5连续递增
# 如果ignore_index=False,索引会是0,1,2,0,1,2(重复)

关键参数解析:

参数 作用 默认值 推荐用法
objs 要拼接的对象列表 必需 使用列表 [df1, df2, ...]
axis 拼接方向(0=行,1=列) 0 axis=0垂直,axis=1水平
ignore_index 忽略原索引,重新生成 False 拼接后通常设为True
keys 创建多级索引标识来源 None 需要追溯数据源时使用
join 列对齐方式(‘inner’/‘outer’) ‘outer’ ’inner’只保留共有列
sort 是否对列排序 True 大数据集设为False提升性能

52.3.2 多级索引的应用

场景:需要追踪每条数据的来源。

列表 52.6
# =============================================================================
# 题目:使用concat创建多级索引
# =============================================================================
# 本任务演示如何使用keys参数在拼接时创建多级索引,以便追溯数据来源
# 场景:多源数据质量检查时,需要快速定位问题数据来自哪个数据源

# ==================== 使用keys参数创建多级索引 ====================
# keys参数:为每个输入的数据框分配一个键值,用于创建多级索引
# 参数说明:
#   [stock_a_returns, stock_b_returns]:要拼接的数据框列表
#   keys=['股票A', '股票B']:为两个数据框分别指定标识键
#   names=['数据源', '行号']:给多级索引的每一级命名
multi_index_returns = pd.concat(
    [stock_a_returns, stock_b_returns],
    keys=['股票A', '股票B'],  # 第一级索引:标识数据来源
    names=['数据源', '行号']  # 给两级索引分别命名
)

# ==================== 打印多级索引结构 ====================
print('多级索引结构:')
print(multi_index_returns)
# 输出解读:
# - 索引变成了两级行:(股票A, 0), (股票A, 1), (股票A, 2), (股票B, 0), (股票B, 1), (股票B, 2)
# - 第一级'数据源'标识数据来自股票A还是股票B
# - 第二级'行号'保留原始数据框的行索引
# - 这种结构便于后续按数据源筛选或分析

print('\n索引信息:')
print(multi_index_returns.index)
# 输出解读:显示多级索引的完整结构,包括索引名称和级别

# ==================== 选择特定来源的数据 ====================
# multi_index_returns.loc['股票A']:使用第一级索引'数据源'进行筛选
# .loc[]:按标签索引,这里只指定第一级索引'股票A',会选出所有第二级索引的数据
print('\n仅选择股票A的数据:')
print(multi_index_returns.loc['股票A'])
# 输出解读:只显示来自股票A的3行数据
# 应用场景:发现某天数据异常时,可以快速定位是哪个数据源的问题

金融应用:多源数据质量检查时,可通过多级索引快速定位问题数据的来源。

52.4 merge函数基于键值的水平合并

52.4.1 基础连接操作

列表 52.7
# =============================================================================
# 题目:使用merge进行基础连接
# =============================================================================
# 本任务演示如何使用pd.merge()函数基于共同的键值(股票代码)进行水平合并
# 场景:将股票基本信息与财务数据合并,得到完整的分析数据集

# ==================== 创建股票基本信息数据 ====================
# 场景:3只A股的基本信息(股票代码、名称、行业)
stock_info = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH'],  # 3只股票的代码
    '股票名称': ['贵州茅台', '五粮液', '招商银行'],  # 对应的股票名称
    '行业': ['食品饮料', '食品饮料', '金融']  # 所属行业
})

# ==================== 创建股票财务数据 ====================
# 场景:3只股票的估值指标(注意:股票代码与基本信息不完全相同)
financial_data = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '601318.SH'],  # 注意:第3只是中国平安,不是招商银行
    'PE': [35.2, 25.8, 10.5],  # 市盈率(Price-to-Earnings ratio)
    'PB': [12.5, 8.3, 1.2]  # 市净率(Price-to-Book ratio)
})

# ==================== 内连接(Inner Join)====================
# pd.merge():基于键值合并两个数据框
# 参数说明:
#   stock_info, financial_data:要合并的两个数据框
#   on='股票代码':指定合并的键值列(两边都有这一列)
#   how='inner':内连接,只保留键值在两边都存在的行
#     - inner(默认):交集,只保留两边都有的键
#     - left:保留左表所有行
#     - right:保留右表所有行
#     - outer:并集,保留所有键
inner_result = pd.merge(
    stock_info,
    financial_data,
    on='股票代码',  # 基于股票代码列进行匹配
    how='inner'  # 内连接,只保留两边都有的股票
)

# ==================== 打印原始数据 ====================
print('股票基本信息:')
print(stock_info)
# 输出解读:包含3只股票(贵州茅台、五粮液、招商银行)

print('\n财务数据:')
print(financial_data)
# 输出解读:包含3只股票(贵州茅台、五粮液、中国平安)
# 注意:招商银行(600036.SH)在基本信息中,但不在财务数据中
#       中国平安(601318.SH)在财务数据中,但不在基本信息中

# ==================== 打印内连接结果 ====================
print('\n内连接结果(只保留两边都有的股票):')
print(inner_result)
# 输出解读:只有2只股票(贵州茅台、五粮液)被保留
# 原因:招商银行和中国平安的代码只在一边出现,被内连接过滤掉了
# 应用场景:确保分析的股票同时具备基本信息和财务数据,避免缺失值

内连接的数学含义:

\[ R \bowtie S = \{ (r, s) \mid r[\text{key}] = s[\text{key}] \} \]

只有键值在两个数据集中都存在的行才会被保留。

52.4.2 连接类型的完整对比

列表 52.8
# =============================================================================
# 题目:四种连接类型的对比
# =============================================================================
# 本任务演示merge函数的四种连接类型(inner/left/right/outer)的区别
# 场景:根据不同的业务需求,选择合适的连接方式整合数据

# ==================== 左连接(Left Join)====================
# how='left':保留左表(stock_info)的所有行
# 右表(financial_data)中匹配不到的行,其列填充为NaN(缺失值)
left_result = pd.merge(stock_info, financial_data, on='股票代码', how='left')
# 输出预期:
# - 贵州茅台:匹配成功,完整数据
# - 五粮液:匹配成功,完整数据
# - 招商银行:左表有但右表无,PE和PB列填充为NaN

# ==================== 右连接(Right Join)====================
# how='right':保留右表(financial_data)的所有行
# 左表(stock_info)中匹配不到的行,其列填充为NaN
right_result = pd.merge(stock_info, financial_data, on='股票代码', how='right')
# 输出预期:
# - 贵州茅台:匹配成功,完整数据
# - 五粮液:匹配成功,完整数据
# - 中国平安:右表有但左表无,股票名称和行业列填充为NaN

# ==================== 外连接(Outer Join)====================
# how='outer':保留所有行(左右表的并集)
# 匹配不到的列都填充为NaN
outer_result = pd.merge(stock_info, financial_data, on='股票代码', how='outer')
# 输出预期:
# - 贵州茅台:匹配成功,完整数据
# - 五粮液:匹配成功,完整数据
# - 招商银行:只在左表,右表的PE、PB列为NaN
# - 中国平安:只在右表,左表的股票名称、行业列为NaN

# ==================== 打印各种连接结果 ====================
print('左连接结果(保留左边所有股票):')
print(left_result)
# 输出解读:招商银行被保留,但其财务指标为NaN
# 应用场景:基本信息是主表,不能丢失任何股票,财务数据只是补充信息

print('\n右连接结果(保留右边所有股票):')
print(right_result)
# 输出解读:中国平安被保留,但其基本信息为NaN
# 应用场景:财务数据是主表,需要确保所有有财务数据的股票都被分析

print('\n外连接结果(保留所有股票,缺失值填充为NaN):')
print(outer_result)
# 输出解读:所有4只股票都被保留,缺失的相应位置填充为NaN
# 应用场景:最大化信息利用,后续可以分析哪些股票缺失哪些数据

连接类型的决策树:

是否需要保留左边所有数据?
├─ 是 → 使用 left join
└─ 否 → 是否需要保留右边所有数据?
    ├─ 是 → 使用 right join
    └─ 否 → 是否需要保留所有数据?
        ├─ 是 → 使用 outer join
        └─ 否 → 使用 inner join (最严格)

金融应用指南:

场景 推荐连接类型 理由
主数据表匹配补充信息 left 保证主表数据不丢失
数据源可靠性相同 inner 只保留两边都有的高质量数据
整合多个不完整来源 outer 最大化信息利用,后续处理缺失值

52.5 join方法索引对齐的便捷工具

列表 52.9
# =============================================================================
# 题目:使用join基于索引合并
# =============================================================================
# 本任务演示如何使用df.join()方法基于索引进行数据合并
# join是merge的特例,专门用于基于索引合并,代码更简洁
# 场景:两个数据框都已将股票代码设为索引,需要基于索引合并

# ==================== 创建以股票代码为索引的数据 ====================
# 场景:收益率数据,以股票代码为行索引
returns = pd.DataFrame({
    '日收益率': [0.02, 0.01, -0.01]  # 3只股票的日收益率
}, index=['600519.SH', '000858.SZ', '600036.SH'])  # 将股票代码设为索引

# 场景:波动率数据,也以股票代码为行索引
# 注意:第3只股票是中国平安(601318.SH),与收益率数据不同
volatility = pd.DataFrame({
    '年化波动率': [0.25, 0.30, 0.20]  # 3只股票的年化波动率
}, index=['600519.SH', '000858.SZ', '601318.SH'])  # 股票代码索引

# ==================== 基于索引进行左连接 ====================
# df.join():基于索引合并两个数据框
# 参数说明:
#   volatility:要合并的右表
#   how='left':左连接,保留左表(returns)的所有索引
#              右表中匹配不到的索引,其列填充为NaN
joined_data = returns.join(volatility, how='left')
# 等价于:pd.merge(returns, volatility, left_index=True, right_index=True, how='left')
# 但join的代码更简洁,专门针对基于索引的合并场景

# ==================== 打印原始数据 ====================
print('收益率数据(以股票代码为索引):')
print(returns)
# 输出解读:3只股票的收益率,索引是股票代码

print('\n波动率数据(以股票代码为索引):')
print(volatility)
# 输出解读:3只股票的波动率,索引也是股票代码
# 注意:招商银行(600036.SH)在收益率中,但不在波动率中
#       中国平安(601318.SH)在波动率中,但不在收益率中

# ==================== 打印基于索引的左连接结果 ====================
print('\n基于索引的左连接结果:')
print(joined_data)
# 输出解读:
# - 贵州茅台(600519.SH):两边都有,完整数据
# - 五粮液(000858.SZ):两边都有,完整数据
# - 招商银行(600036.SH):只在左表,年化波动率列为NaN
# - 中国平安(601318.SH):不在结果中,因为左连接不保留右表独有的索引

join vs merge的选用原则:

  • 使用join: 数据已经以键值为索引,代码更简洁
  • 使用merge: 需要基于列进行连接,或需要更复杂的连接条件

52.6 金融应用多源数据整合案例

52.6.1 场景上市公司多维数据整合

任务:整合股票基本信息、行情数据、财务指标,构建完整的分析数据集。

列表 52.10
# =============================================================================
# 题目:金融多源数据整合实战
# =============================================================================
# 本任务演示如何逐步整合多个数据源,构建完整的股票分析数据集
# 场景:整合股票基本信息、日行情数据、季度财务指标

# ==================== 导入必要的库 ====================
import pandas as pd

# ==================== 数据源1:股票基本信息 ====================
# 场景:4只A股的基本信息(代码、名称、上市日期、行业)
# 这是主表,后续合并时以这个表为基础(左连接)
stock_basic = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH'],
    '股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安'],
    '上市日期': ['2001-08-27', '1998-04-27', '2002-04-09', '2007-03-01'],
    '行业': ['食品饮料', '食品饮料', '金融', '金融']
})

# ==================== 数据源2:日行情数据(某日)====================
# 场景:某日的收盘价和涨跌幅数据
# 注意:只有3只股票有行情数据,中国平安缺失
daily_quote = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '600036.SH'],
    '收盘价': [1850.00, 158.50, 32.80],  # 当日收盘价(元)
    '涨跌幅': [1.5, -0.8, 0.5]  # 当日涨跌幅(%)
})

# ==================== 数据源3:财务指标(季频)====================
# 场景:最新季度的财务指标(ROE、负债率)
# 注意:只有3只股票有财务数据,招商银行缺失
financial_metrics = pd.DataFrame({
    '股票代码': ['600519.SH', '000858.SZ', '601318.SH'],
    'ROE': [25.8, 22.3, 15.6],  # 净资产收益率(Return on Equity,%)
    '负债率': [18.5, 30.2, 92.5]  # 资产负债率(%)
})

# ==================== 步骤1:以基本信息为主表,左连接行情数据 ====================
# pd.merge():基于股票代码合并基本信息和行情数据
# 参数说明:
#   stock_basic:左表(主表),包含所有股票的基本信息
#   daily_quote:右表,包含当日的行情数据
#   on='股票代码':基于股票代码列进行匹配
#   how='left':左连接,保留左表(基本信息)的所有股票
#              右表中匹配不到的股票,其行情数据列填充为NaN
#   indicator=True:添加一列'_merge',标识每行数据的来源
#                  - 'both':两边都有
#                  - 'left_only':只在左表
#                  - 'right_only':只在右表
step1 = pd.merge(
    stock_basic,
    daily_quote,
    on='股票代码',
    how='left',  # 左连接,确保所有股票都被保留
    indicator=True  # 添加_merge列标识数据来源
)

print('步骤1:基本信息 + 行情数据(左连接)')
print(step1)
# 输出解读:中国平安的收盘价和涨跌幅为NaN(因为行情数据中没有这只股票)

# ==================== 步骤2:继续左连接财务指标 ====================
# pd.merge():将步骤1的结果与财务数据继续合并
# 参数说明:
#   step1:左表,已经包含基本信息+行情数据
#   financial_metrics:右表,包含财务指标
#   on='股票代码':继续基于股票代码匹配
#   how='left':左连接,保留左表的所有股票
#   suffixes=('', '_财务'):处理列名冲突
#                        - 如果两个数据框有重名列,分别添加后缀区分
#                        - 这里只是示例,实际没有重名列
final_data = pd.merge(
    step1,
    financial_metrics,
    on='股票代码',
    how='left',
    suffixes=('', '_财务')  # 处理潜在的列名冲突
)

print('\n最终整合结果:')
print(final_data)
# 输出解读:
# - 贵州茅台、五粮液:完整数据(基本信息、行情、财务都有)
# - 招商银行:缺失财务指标(ROE和负债率为NaN)
# - 中国平安:缺失行情数据(收盘价和涨跌幅为NaN)

# ==================== 分析数据完整性 ====================
print('\n数据完整性分析:')
# 选择关键列并重命名,方便阅读
# []:选择列,.rename():重命名列
print(final_data[['股票代码', '股票名称', '_merge']].rename(columns={'_merge': '行情数据'}))
# 输出解读:_merge列显示哪些股票有行情数据('both'),哪些没有('left_only')

print('\n缺失值统计:')
# .isna().sum():统计每列的缺失值数量
print(final_data.isna().sum())
# 输出解读:收盘价、涨跌幅各有1个缺失(中国平安),ROE、负债率各有1个缺失(招商银行)

数据整合的关键决策:

  1. 主表选择:以股票基本信息为主表,使用left join确保每只股票都保留
  2. 数据来源追踪:使用indicator=True标识每条数据是否成功匹配
  3. 列名冲突:使用suffixes参数处理重名列
  4. 缺失值处理:财务指标缺失可能意味着该股票尚未发布财报

52.6.2 性能优化策略

大数据集合并的性能陷阱:

列表 52.11
# =============================================================================
# 题目:大数据集合并的性能优化
# =============================================================================
# 本任务演示如何优化大规模数据集的合并性能
# 场景:500万行行情数据与5000行财务数据的合并

# ==================== 导入必要的库 ====================
import pandas as pd
import numpy as np
import time  # 用于计时的库

# ==================== 创建大规模测试数据 ====================
n_stocks = 5000  # 股票数量
n_dates = 1000  # 交易日期数量

# 生成股票行情数据(500万行)
# 场景:5000只股票在1000个交易日的收盘价数据
quotes = pd.DataFrame({
    # 股票代码列:每只股票的代码重复1000次(对应1000个交易日)
    # np.repeat():重复数组,[f'{i:06d}.SH' for i in range(n_stocks)]生成股票代码列表
    #              每个代码重复n_dates次
    '股票代码': np.repeat([f'{i:06d}.SH' for i in range(n_stocks)], n_dates),
    # 日期列:1000个日期重复5000次
    # list(pd.date_range(...)) * n_stocks:将日期列表复制5000次
    '日期': list(pd.date_range('2020-01-01', periods=n_dates)) * n_stocks,
    # 收盘价列:生成500万个10到100之间的随机数
    '收盘价': np.random.uniform(10, 100, n_stocks * n_dates)
})

# 生成财务数据(5000行)
# 场景:5000只股票的季度财务指标
financials = pd.DataFrame({
    '股票代码': [f'{i:06d}.SH' for i in range(n_stocks)],  # 5000只股票的代码
    'ROE': np.random.uniform(5, 30, n_stocks),  # ROE:5%到30%之间的随机数
    '市值': np.random.uniform(50, 5000, n_stocks)  # 市值:50亿到5000亿之间的随机数
})

# ==================== 方法1:未优化的合并 ====================
# 场景:直接合并,不进行任何优化处理
print('开始未优化的合并...')
start_time = time.time()  # 记录开始时间
# pd.merge():基于股票代码合并500万行行情数据和5000行财务数据
# 默认情况下,Pandas会对合并键进行排序,这在数据量大时很耗时
result_slow = pd.merge(quotes, financials, on='股票代码')
slow_time = time.time() - start_time  # 计算耗时

# ==================== 方法2:优化后的合并(设置数据类型)====================
# 优化策略1:将字符串类型的键值转换为category类型
# category类型使用整数编码,比较和匹配的速度更快
quotes_opt = quotes.copy()
# .astype('category'):将股票代码列转换为category类型
#                   - 对于重复值多的列(如500万行中只有5000个唯一值),效率提升显著
quotes_opt['股票代码'] = quotes_opt['股票代码'].astype('category')

financials_opt = financials.copy()
financials_opt['股票代码'] = financials_opt['股票代码'].astype('category')

print('开始优化后的合并...')
start_time = time.time()
# pd.merge():合并优化后的数据
result_fast = pd.merge(quotes_opt, financials_opt, on='股票代码')
fast_time = time.time() - start_time

# ==================== 性能对比 ====================
print(f'未优化合并时间: {slow_time:.2f}秒')
print(f'优化后合并时间: {fast_time:.2f}秒')
print(f'性能提升: {slow_time/fast_time:.1f}倍')
# 输出解读:优化后的合并通常能提升2-5倍的性能
# 提升幅度取决于数据规模和硬件配置

性能优化清单:

键值类型优化:将字符串键值转换为category类型 ✅ 索引优化:对键值列建立索引(df.set_index()) ✅ 避免重复:合并前检查并删除重复数据 ✅ 分块处理:超大文件考虑分块读取和合并 ✅ 使用Dask:超出内存容量时使用并行计算框架

52.7 高级主题复杂连接条件

52.7.1 多键连接

列表 52.12
# =============================================================================
# 题目:基于多个键值进行连接
# =============================================================================
# 本任务演示如何基于多个列(股票代码+日期)进行合并
# 场景:需要精确匹配股票和日期,确保同一股票在同一天的行情和财务数据合并

# ==================== 创建包含日期的行情数据 ====================
# 场景:两只股票在两个交易日的收盘价数据
quotes = pd.DataFrame({
    '股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
    '日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
    '收盘价': [1850.0, 1870.0, 158.5]  # 注意:五粮液只有1月1日的数据
})

# ==================== 创建包含日期的财务数据 ====================
# 场景:两只股票在两个交易日的估值指标
financals = pd.DataFrame({
    '股票代码': ['600519.SH', '600519.SH', '000858.SZ'],
    '日期': ['2024-01-01', '2024-01-02', '2024-01-01'],
    'PE': [35.2, 35.8, 25.8]  # 市盈率数据
})

# ==================== 基于股票代码和日期两个键进行合并 ====================
# pd.merge():多键连接
# 参数说明:
#   on=['股票代码', '日期']:指定多个键值列
#                          - 只有当两个键都匹配时,行才会被连接
#                          - 相当于 SQL 的 ON a.股票代码=b.股票代码 AND a.日期=b.日期
#   how='inner':内连接,只保留两边都有的行
merged = pd.merge(
    quotes,
    financals,
    on=['股票代码', '日期'],  # 多键连接:同时匹配股票代码和日期
    how='inner'
)

print('多键连接结果:')
print(merged)
# 输出解读:3行数据都成功匹配
# - 贵州茅台1月1日:股票代码和日期都匹配
# - 贵州茅台1月2日:股票代码和日期都匹配
# - 五粮液1月1日:股票代码和日期都匹配
# 应用场景:确保分析的是同一股票在同一天的完整数据
#           避免错误地将不同日期的数据拼接在一起

多键连接的数学含义:

\[ R \bowtie_{k_1, k_2} S = \{ (r, s) \mid r[k_1] = s[k_1] \land r[k_2] = s[k_2] \} \]

只有所有指定的键值都匹配时,两行数据才会被连接。